package de.jmda.gui.swing.mvc;

import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;

import de.jmda.gen.Generator;
import de.jmda.gen.LineIndenter;
import de.jmda.mproc.ProcessingUtilities;
import de.jmda.mproc.Processor;
import de.jmda.util.StringUtil;

public class ControllerSupportEnabledGenerator implements Generator
{
	private final static Logger LOGGER =
			Logger.getLogger(ControllerSupportEnabledGenerator.class);

	private final static File CLASS_PROPERTIES_FILE =
			new File(
					ControllerSupportEnabledGenerator.class.getName() + ".properties");

	private final static File   SRC_DIR;
	private final static String SRC_DIR_KEY     = "SRC_DIR";
	private final static File   SRC_DIR_DEFAULT = new File("./src/test/java");

	private final static String  PATTERN_PREFIX =
			"//GEN-BEGIN:controller support";
	private final static String  PATTERN_SUFFIX =
			"//GEN-END:controller support";
	private final static Pattern PATTERN        =
			Pattern.compile(PATTERN_PREFIX + ".*" + PATTERN_SUFFIX, Pattern.DOTALL);

	static
	{
		// initialise constants from class properties
		PropertiesConfiguration configuration;

		try
		{
			configuration = new PropertiesConfiguration(CLASS_PROPERTIES_FILE);
		}
		catch (ConfigurationException e)
		{
			throw new ExceptionInInitializerError(
					"failure loading property configuration\n" + e);
		}

		if (configuration.isEmpty())
		{
			configuration.setProperty(SRC_DIR_KEY, SRC_DIR_DEFAULT.getAbsolutePath());

			try
			{
				configuration.save(CLASS_PROPERTIES_FILE);
			}
			catch (ConfigurationException e)
			{
				LOGGER.warn("failure writing class properties file", e);
			}
		}

		SRC_DIR = new File((String) configuration.getProperty(SRC_DIR_KEY));
	}

	private LineIndenter lineIndenter;

	private TypeElement typeElement;

	/**
	 * @param typeElement
	 */
	public ControllerSupportEnabledGenerator(TypeElement typeElement)
  {
		super();
		this.typeElement = typeElement;
  }

	@Override
	public StringBuffer generate()
	{
		// create source file object
		File srcFile = Processor.getSourceFile(typeElement);
		
		if (srcFile == null)
		{
			srcFile =
					new File(
							SRC_DIR.getAbsolutePath() + "/" +
									typeElement.getQualifiedName().toString().replace('.', '/') +
									".java");
		}

		if (srcFile.exists())
		{
			LOGGER.info("source file exists");

			try
			{
				// read source file
				String srcFileContent = FileUtils.readFileToString(srcFile);

				// try to find generated controller support (embraced within
				// PATTERN_PREFIX and PATTERN_SUFFIX as defined in PATTERN)
				Matcher matcher = PATTERN.matcher(srcFileContent);

				if (matcher.find())
				{
					// replace generated controller support
					srcFileContent = replaceControllerSupport(matcher);
				}
				else
				{
					// insert controller support
					srcFileContent = insertControllerSupport(srcFileContent);
				}

				// rewrite source file with new content that also contains controller
				// support
				FileUtils.writeStringToFile(srcFile, srcFileContent);

				// create controller context source file object
				File targetFileControllerContext =
						new File(
								srcFile.getParent() + "/" +
										typeElement.getSimpleName()
//								SRC_DIR.getAbsolutePath() + "/" +
//										typeElement.getQualifiedName()
										           .toString().replace('.', '/') +
										"ControllerContext.java");

				// write controller context source file
				FileUtils.writeStringToFile(
						targetFileControllerContext,
						getTargetFileControllerContextContent());

				// create controller file object
				File targetFileController =
						new File(
								srcFile.getParent() + "/" +
										typeElement.getSimpleName()
//								SRC_DIR.getAbsolutePath() + "/" +
//										typeElement.getQualifiedName()
										           .toString().replace('.', '/') +
										"Controller.java");

				if (targetFileController.exists() == false)
				{
					// write new controller file
					FileUtils.writeStringToFile(
							targetFileController, getTargetFileControllerContent());
				}
			}
			catch (IOException e)
			{
				LOGGER.error("failure generating controller support", e);
			}
		}
		else
		{
			LOGGER.info("source file [" + srcFile + "] does not exist");
		}
		
		return new StringBuffer("");
	}

	private String replaceControllerSupport(Matcher matcher)
	{
		return matcher.replaceFirst(getControllerContext());
	}

	private String insertControllerSupport(String srcFileContent)
	{
		int lastIndex = srcFileContent.lastIndexOf('}');

		return
				srcFileContent.substring( 0, lastIndex) +
				"\t" + getControllerContext() + "\n}";
	}

	private String getControllerContext()
	{
		String controllerClassName =
				typeElement.getQualifiedName() + "Controller";
		return
				PATTERN_PREFIX + "                                                                                   \n" +
				"	@" + ControllerSupportDisabled.class.getName() + "                                                 \n" +
				"	private " + controllerClassName + " controller =                                                   \n" +
				"			new " + controllerClassName + "(this);                                                         \n" +
				"                                                                                                    \n" +
				"	/**                                                                                                \n" +
				"	 * @see " + ControllerContextProvider.class.getName() + "#getControllerContext()                   \n" +
				"	 */                                                                                                \n" +
				"	@Override // make sure that this class implements " + ControllerContextProvider.class.getName() + "\n" +
				"	public " + ControllerContext.class.getName() + " getControllerContext()                            \n" +
				"	{                                                                                                  \n" +
				"		return new " + getConstructorCall() + ";                                                         \n" +
				"	}                                                                                                  \n" +
				"	" + PATTERN_SUFFIX;
	}

	private String getConstructorCall()
	{
		StringBuffer result =
				new StringBuffer(typeElement.getQualifiedName() +
						"ControllerContext(\n" +
						"\t\t\t\tthis,\n");

		for (VariableElement variableElement :
		     ElementFilter.fieldsIn(typeElement.getEnclosedElements()))
		{
			if (variableElement.getAnnotation(ControllerSupportDisabled.class) ==
			    null)
			{
				result.append("\t\t\t\t" + variableElement.getSimpleName() + ",\n");
			}
		}

		return StringUtil.rTrimChars(result.toString(), ",\n") + ")";
	}

	private String getTargetFileControllerContextContent()
	{
		String simpleClassName =
				typeElement.getSimpleName() + "ControllerContext";
		
		StringBuffer result =
				new StringBuffer(
						"package " + ProcessingUtilities.getPackageQualifiedName(typeElement) +
								";\n\n" +
						"/**\n" +
						" * This class was generated by " + this.getClass().getName() +
								", do not edit!\n" +
						" */\n" +
						"public class " + simpleClassName + "\n" +
						"\t\timplements " + ControllerContext.class.getName() + "\n" +
						"{\n" +
						getFieldDeclarations() + "\n" +
						"\tpublic " + simpleClassName + "(\n" +
								getConstructorParameters() + ")\n" +
						"\t{\n" +
								getFieldAssignments() + "\n" +
						"\t}\n\n" +
						getFieldGetters() + "\n" +
						"}");

		return result.toString();
	}

	private String getFieldDeclarations()
	{
		StringBuffer result =
				new StringBuffer(
						"\tprivate " + typeElement.getQualifiedName() + " " +
						StringUtil.firstLetterToLowerCase(
								typeElement.getSimpleName().toString()) + ";\n");

		for (VariableElement field : ProcessingUtilities.getFields(typeElement))
		{
			if (field.getAnnotation(ControllerSupportDisabled.class) ==  null)
			{
				result.append(
						"\tprivate " + field.asType() + " " + field.getSimpleName() + ";\n");
			}
		}

		return result.toString();
	}

	private String getConstructorParameters()
	{
		StringBuffer result =
				new StringBuffer(
						"\t\t\t" + typeElement.getQualifiedName() + " " +
						StringUtil.firstLetterToLowerCase(
								typeElement.getSimpleName().toString()) + ",\n");

		for (VariableElement field : ProcessingUtilities.getFields(typeElement))
		{
			if (field.getAnnotation(ControllerSupportDisabled.class) ==
					null)
			{
				result.append(
						"\t\t\t" + field.asType() + " " + field.getSimpleName() + ",\n");
			}
		}

		return StringUtil.rTrimChars(result.toString(), ",\n");
	}

	private String getFieldAssignments()
	{
		StringBuffer result =
				new StringBuffer(
						"\t\tthis." + StringUtil.firstLetterToLowerCase(
								typeElement.getSimpleName().toString()) + " = " +
						StringUtil.firstLetterToLowerCase(
								typeElement.getSimpleName().toString()) + ";\n");

		for (VariableElement field : ProcessingUtilities.getFields(typeElement))
		{
			if (field.getAnnotation(ControllerSupportDisabled.class) ==
					null)
			{
				String fieldDeclarationSimpleName = field.getSimpleName().toString();

				result.append(
						"\t\tthis." + fieldDeclarationSimpleName + " = " +
								fieldDeclarationSimpleName + ";\n");
			}
		}

		return result.toString();
	}

	private String getFieldGetters()
	{
		StringBuffer result =
				new StringBuffer(
						"\tpublic " + typeElement.getQualifiedName() + " get" +
								typeElement.getSimpleName() + "()\n" +
						"\t{" +
						"\t\treturn " +
						StringUtil.firstLetterToLowerCase(
								typeElement.getSimpleName().toString()) + ";\n" +
						"\t}\n\n");

		for (VariableElement field : ProcessingUtilities.getFields(typeElement))
		{
			if (field.getAnnotation(ControllerSupportDisabled.class) ==
					null)
			{
				String fieldSimpleName = field.getSimpleName().toString();

				result.append(
						"\tpublic " + field.asType() +
								" get" + StringUtil.firstLetterToUpperCase(
										fieldSimpleName) + "()\n" +
						"\t{\n" +
						"\t\treturn " + fieldSimpleName + ";\n" +
						"\t}\n\n");
			}
		}

		return StringUtil.rTrimChars(result.toString(), "\n\n");
	}

	private String getTargetFileControllerContent()
	{
		String simpleClassName =
				typeElement.getSimpleName() + "Controller";
//		String controllerContextFieldName =
//				StringUtil.firstLetterToLowerCase(simpleClassName) + "Context";
		String controllerContextClassName =
				typeElement.getSimpleName() + "ControllerContext";
		String modelClassName =
				typeElement.getSimpleName() + "Model";

		StringBuffer result =
				new StringBuffer(
"package " + ProcessingUtilities.getPackageQualifiedName(typeElement) +";               \n" +
"                                                                                       \n" +
"/**                                                                                    \n" +
" * This class was initially generated by {@link " + this.getClass().getName() + "}.    \n" +
" * <p>                                                                                 \n" +
" * <code>" + this.getClass().getSimpleName() + "</code> will not regenerate this class,\n" +
" * hence modifications will not get lost due to subsequent generation by <code>" +
		this.getClass().getSimpleName() + "</code>.                                         \n" +
" */                                                                                    \n" +
"class " + simpleClassName + "                                                          \n" +
"		extends " + AbstractController.class.getName() + "<" + modelClassName + ">          \n" +
"{                                                                                      \n" +
"	private " + controllerContextClassName + " context;                                   \n" +
"                                                                                       \n" +
"	private " + modelClassName + " model;                                                 \n" +
"                                                                                       \n" +
"	/**                                                                                   \n" +
"	 * Constructor                                                                        \n" +
"	 */                                                                                   \n" +
"	" + simpleClassName + "(                                                              \n" +
"			" + ControllerContextProvider.class.getName() + " controllerContextProvider)      \n" +
"	{                                                                                     \n" +
"		super(controllerContextProvider);                                                   \n" +
"		context = (" + controllerContextClassName + ") controllerContextProvider.getControllerContext();\n" +
"	}                                                                                     \n" +
"                                                                                       \n" +
"	@Override                                                                             \n" +
"	public void refreshControllerContext()                                                \n" +
"	{                                                                                     \n" +
"		context = (" + controllerContextClassName + ") controllerContextProvider.getControllerContext();\n" +
"	}                                                                                     \n" +
"                                                                                       \n" +
"	@Override                                                                             \n" +
"	public void setModel(" + modelClassName + " model)                                    \n" +
"	{                                                                                     \n" +
"		this.model = model;                                                                 \n" +
"	}                                                                                     \n" +
"}");

		return result.toString();
	}

	@Override
  public void setLineIndenter(LineIndenter lineIndenter)
  {
		this.lineIndenter = lineIndenter;
  }

	@Override
  public LineIndenter getLineIndenter()
  {
	  return lineIndenter;
  }
}